﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Outfish {

	static public class CssFactory {

		/// <summary>
		/// Parses a (multi) step, (multi) path CSS-selector string into a DescentFinder
		/// </summary>
		/// <example>
		/// IDescendantFinder finder = "div img a td:even, div[header]".ParseCssPathFinder();
		/// </example>
		static public IDescendantFinder ParseCssPathFinder( this string multiPath ){
			return Parsing.ParseMultiPath( multiPath );
		}

		/// <summary>
		/// Parses a single-step CSS-selector string for finding single HtmlNodes
		/// </summary>
		/// <example>
		/// INodeSelector selector = "tagName#id.class:empty".ParseCssNodeSelector()
		/// </example>
		static public INodeSelector ParseCssNodeSelector(this string step){
			return Parsing.ParseCssNodeSelector( step );
		}

	}
	
	
	public class Parsing {

		static public INodeSelector ParseCssNodeSelector(string stepCss){
			int index = 0;
			var tokens = Lexer.ParseTokens( stepCss );
			return ConsumeStep( tokens, ref index );
		}

		static internal CssPathSelector ParseSinglePath( string pathCss ){
			int index = 0;
			var tokens = Lexer.ParseTokens( pathCss );
			return ConsumePath( tokens, ref index );
		}

		static public IDescendantFinder ParseMultiPath( string multiPathCss ){
			int index = 0;
			var tokens = Lexer.ParseTokens( multiPathCss );
			return ConsumeMultiPath( tokens, ref index );
		}

		static IDescendantFinder ConsumeMultiPath( List<Token> tokens, ref int index ){

			List<CssPathSelector> paths = new List<CssPathSelector>();

			// get path
			paths.Add( ConsumePath( tokens, ref index ) );
		
			while( index < tokens.Count ){
				var t = tokens[index];
				// consume comma
				if( t.Type != TokenType.Comma ){
					throw new Exception("Unexpected token:"+t.Text);
				}
				++index;

				// if they put a comma, there ought to be something following it...
				paths.Add( ConsumePath( tokens, ref index ) );
			}
			
			if( paths.Count == 1 ){ return paths[0]; }
			return new CssMultiPathSelector( paths );

		}

		// consumes steps and whitespace from token stream until EOS or ',' is reached
		static CssPathSelector ConsumePath( List<Token> tokens, ref int index ){
		
			List<StepSelector> steps = new List<StepSelector>();
			
			bool bContinue = true;
			bool isImmediateChild = false;
			string siblingOp = null;
			StepSelector last = null;
			while( index < tokens.Count && bContinue ){
				var t = tokens[index];
				switch( t.Type ){
				
					case TokenType.Comma: 
						bContinue = false; 
						break;
						
					case TokenType.Whitespace: 
						++index; 
						break;
						
					case TokenType.Relationship:
						if( last == null ){ throw new Exception("Error starting selector path with relationship ( '>' '+' '~' ) operator."); }
						if( t.Text == ">" ){
							isImmediateChild = true; // set flag so next selector added, adds its self to parent as immediate
						} else {
							siblingOp = t.Text; // set flag so when next node gets added, it can strip off the previous node and use as a sibling instead
						}
						++index;
						break;
						
					default: 
						// add new step
						INodeSelector ns = ConsumeStep( tokens, ref index );
						var cur = new StepSelector( ns );
						steps.Add( cur ); 
						
						// check last/cur have a relationship
						if( isImmediateChild ){
							var pureParentSelector = last.Selector; // grab this before it gets soiled.
							last.AddChild( ns ); // this part helps pair down the tree faster
							cur.AddParent( pureParentSelector ); // this part is required to make it work
							isImmediateChild = false;
						}
						if( siblingOp != null ){
							steps.Remove( last ); // remove last because it is not a parent
							cur.AddSibling( last.Selector, siblingOp == "~" ); // it is a sibling
							siblingOp = null;
						}
						
						last = cur;
						break;
				}
			}

			return new CssPathSelector( steps );
		}

		// consumes tokens until EOS or non-step token is reached
		static INodeSelector ConsumeStep( List<Token> tokens, ref int index ){
			int startIndex = index;
			
			List<INodeSelector> partialSelectorList = new List<INodeSelector>();
			bool bContinue = true;
			INodeSelector partial;
			while( index < tokens.Count && (partial = ConsumePartialStep(tokens, ref index))!=null ){
				partialSelectorList.Add( partial );
			} // while
			
			switch( partialSelectorList.Count ){
				case 0: throw new Exception("No step-tokens provided");
				case 1: return partialSelectorList[ 0 ];
				default: return new AndSelector( partialSelectorList );
			}
			
		}

		// consume exactly 1 partial-step
		static INodeSelector ConsumePartialStep( List<Token> tokens, ref int index ){
		
			Token t = tokens[index];
			switch( t.Type ){
			
				case TokenType.Name:   ++index;    return new NameSelector( t.Text );
				case TokenType.Class:  ++index;    return AttributeSelector.ContainsClass( t.Text.Substring(1) );
				case TokenType.Id:     ++index;    return AttributeSelector.IdIs( t.Text.Substring(1) );
				case TokenType.Square: index += 3; return ParseAttribute( tokens[index-2].Text ); 
				case TokenType.Pseudo: 
				
					switch( t.Text ){
						case ":even":       ++index; return SiblingSelector.Even;
						case ":odd":		++index; return SiblingSelector.Odd;
						case ":nth-child":	index += 4; return ParseSibling( tokens[index-2].Text );  
						case ":contains":	index += 4; return new ContainsTextSelector( tokens[index-2].Text );
						default:			index += 4; return ParseColon( t.Text );
					}
				
			} // switch

			return null;
		
		}

		static SiblingSelector ParseSibling(string s){
			Match m=Regex.Match(s,@"\s*((\d+)n\+(\d+)|(\d+)n|(\d))\s*",RegexOptions.IgnoreCase);
			if( m.Success == false ){ throw new ArgumentNullException("can't parse nth-child for "+s); }
			
			string divisor = m.Groups[4].Value + m.Groups[2].Value;	// at most 1 will have a # 
			string remain = m.Groups[5].Value + m.Groups[3].Value;	// at most 1 will have a # 
			int d = divisor.Length>0 ? int.Parse( divisor ) : 1;
			int r = remain.Length>0 ? int.Parse( remain ) : 0;
			return new SiblingSelector( d, r );
		}

		static PredicateSelector ParseColon( string src ){
			var pred = ParsePredicate( src );
			return new PredicateSelector( pred, src );
		}

		static string DetermineSubstring( string containsParam ){
			if( containsParam.Length == 0 ){ return containsParam; } // 0-length
		
			// either properly quoted or self ending at ')'
			char first = containsParam[0];
			// not quoted (and must have been pre-trimmed)
			if( first != '\'' && first != '"' ){ return containsParam; }
			
			// if we needed to validate quotes, do it here
			// but I don't think we need to since we parsed the token ourselves
			
			return containsParam.Substring( 1, containsParam.Length-2 );
			
		}

		static Predicate<HtmlNode> ParsePredicate(string s){
			switch( s ){
				case ":first-child": return PredicateSelector.IsFirstChild;
				case ":last-child":	 return PredicateSelector.IsLastChild;
				case ":only-child":	 return PredicateSelector.IsOnlyChild;
				case ":parent":		 return PredicateSelector.IsParent;
				case ":empty":		 return PredicateSelector.IsEmpty;
				
				default: throw new Exception("Colon selector " + s + " not implemented.");
			}
			
			#region don't need / not going to do

			//-- common attr short cuts --
			//case "checked":
			//case "selected":
			//case "disabled":
			//case "hidden":
			//case "visible":

			// -- don't need, just short cut for node name (why?)
			//case "button":
			//case "file":       //    elements of type file
			//case "header":		//         h1,h2,h3 etc
			//case "image":
			//case "input":
			//case "password":
			//case "radio":
			//case "reset":
			//case "submit":
			//case "text":
			//case "checkbox":
			
			//-- don't need, we have linq --
			//case "first":
			//case "last":
			//case "eq(n)":         the n element in the result set
			//case "gt(n)":
			//case "lt(n)":

			// --- not going to do			
			//:animated       element is animated
			//:focus      element has focus
			//:not(selector) negates selector
			
		#endregion
			
		}

		static AttributeSelector ParseAttribute( string src ){
			string name = null;
			string op = null;
			string value = null;

			// first char:  _  : A-Za-z
			// remaining chars:  A-Za-z0-9_=:.
			Match m = Regex.Match(src
				,@"(	[A-Za-z:_][A-Za-z0-9\-_:\.]*	)	(([|*~$!^]?=)	(""[^""]+""|'[^']+'|[^""']+))?"
				,RegexOptions.IgnorePatternWhitespace
			);
			
			if(!m.Success){ throw new FormatException("Can't parse Attribute selector " + src); }
			
			// name
			name = m.Groups[1].Value;
			if( m.Groups[2].Success ){
				// value - strip quotes
				op = m.Groups[3].Value;
				string v = m.Groups[4].Value;
				// strip off quotes
				if(v.Length>2 && ( v[0] == '\'' || v[0] == '"' ) ){ v = v.Substring(1,v.Length-2); }
				value = v;
			} else {
				op = AttributeSelector.ExistsOp;
			}
			return new AttributeSelector( name, op, value );
		}

	}

	
}
